/**
 *  Copyright 2007-2008 University Of Southern California
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package edu.isi.pegasus.planner.catalog.replica;

import edu.isi.pegasus.planner.catalog.classes.CatalogEntry;
import java.util.*;

/**
 * The entry is a high-level logical structure representing the physical
 * filename, the site handle, and optional attributes related to the PFN
 * as one entity.<p>
 *
 * The resource handle is the most frequently used attribute. In
 * reality, the resource handle may be a relational attribute of the
 * mapping relation between an LFN and a PFN - there is disagreement
 * among the developers on this issue. For simplicity purposes, it
 * appears to be sufficient to make the resource handle a regular PFN
 * attribute.<p>
 *
 * @author Jens-S. Vöckler
 * @author Karan Vahi
 * @version $Revision: 2076 $
 */
public class ReplicaCatalogEntry implements CatalogEntry
{
  /**
   * The (reserved) attribute name used for the resource handle.
   */
  public static final String RESOURCE_HANDLE = "pool";

  /**
   * The physical filename.
   */
  private String m_pfn;

  /**
   * Any optional attributes associated with the PFN.
   */
  private Map m_attributeMap;

  /**
   * Default constructor for arrays. The PFN is initialized to
   * <code>null</null>, and thus must be explicitly set later. The map
   * of attributes associated with the PFN is initialized to be empty.
   * Thus, no resource handle is available.
   */
  public ReplicaCatalogEntry()
  {
    m_pfn = null;
    m_attributeMap = new TreeMap();
  }

  /**
   * Convenience constructor initializes the PFN. The map of attributes
   * is initialized to be empty. Thus, no resource handle is avaiable.
   *
   * @param pfn is the PFN to remember.
   */
  public ReplicaCatalogEntry( String pfn )
  {
    m_pfn = pfn;
    m_attributeMap = new TreeMap();
  }

  /**
   * Convenience constructor initializes the PFN and the resource
   * handle. The resource handle is stored as regular PFN attribute.
   *
   * @param pfn is the PFN to remember.
   * @param handle is the resource handle to remember.
   */
  public ReplicaCatalogEntry( String pfn, String handle )
  {
    m_pfn  = pfn;
    m_attributeMap = new TreeMap();
    m_attributeMap.put( RESOURCE_HANDLE, handle );
  }

  /**
   * Standard constructor initializes the PFN and arbitrary attributes.
   *
   * @param pfn is the PFN to remember.
   * @param attributes is a map of arbitrary attributes related to the
   * PFN.
   */
  public ReplicaCatalogEntry( String pfn, Map attributes )
  {
    m_pfn  = pfn;
    m_attributeMap = new TreeMap(attributes);
  }

  /**
   * Adds an attribute to the set of attributes. Note, this is identical
   * to the {@link #setAttribute( String, Object )} method of the same
   * signature.
   *
   * @param key is the key denoting an attribute.
   * @param value is a value object to store.
   */
  public void addAttribute( String key, Object value )
  {
    this.m_attributeMap.put( key, value );
  }

  /**
   * Adds attributes to the existing attributes.
   *
   * @param attributes is a map of attributes to add.
   * @see #setAttribute(Map)
   * @see java.util.Map#putAll( Map )
   */
  public void addAttribute( Map attributes )
  {
    this.m_attributeMap.putAll(attributes);
  }

  /**
   * Obtains the attribute value for a given key.
   *
   * @param key is the key to look up
   * @return the object stored as value, may be null.
   * @see java.util.Map#get( Object )
   */
  public Object getAttribute( String key )
  {
    return this.m_attributeMap.get(key);
  }


  /**
   * Checks for the existence of an attribute key.
   *
   * @param key is the key to look up
   * @return true if the key is known, false otherwise.
   */
  public boolean hasAttribute( String key )
  {
    return this.m_attributeMap.containsKey(key);
  }

  /**
   * Counts the number of attributes known for the PFN.
   *
   * @return number of attributes, may be zero.
   * @see java.util.Map#size()
   */
  public int getAttributeCount()
  {
    return this.m_attributeMap.size();
  }

  /**
   * Provides an iterator to traverse the attributes by their keys.
   *
   * @return an iterator over the keys to walk the attribute list.
   */
  public Iterator getAttributeIterator()
  {
    return this.m_attributeMap.keySet().iterator();
  }

  /**
   * Merges the attribute maps of two entries in a controlled fashion.
   * Entries are only merged with another entry, if the physical
   * filenames match.
   *
   * @param a is one replica catalog entry to merge.
   * @param b is the other replica catalog entry to merge.
   * @param overwrite resolves intersections. If true, uses rce's
   * attribute to remain, if false, the original attribute remains.
   * @return the merged entry, if the PFNs matched, or <code>null</code>
   * if the PFN mismatched.
   */
  public static ReplicaCatalogEntry merge( ReplicaCatalogEntry a,
					   ReplicaCatalogEntry b,
					   boolean overwrite )
  {
    ReplicaCatalogEntry result = null;

    String pfn1 = a.getPFN();
    String pfn2 = b.getPFN();
    if ( pfn1 == null && pfn2 == null ||
	 pfn1 != null && pfn2 != null && pfn1.equals(pfn2) ) {
      result = new ReplicaCatalogEntry( pfn1, a.m_attributeMap );
      result.merge( b, overwrite ); // result cannot be false
    }

    // will return null on PFN mismatch
    return result;
  }

  /**
   * Merges the attribute maps in a controlled fashion. An entry is only
   * merged with another entry, if the physical filenames match.
   *
   * @param rce is another replica catalog entry to merge with.
   * @param overwrite resolves intersections. If true, uses rce's
   * attribute to remain, if false, the original attribute remains.
   * @return true if a merge was attempted, false if the PFNs did not
   * match.
   */
  public boolean merge( ReplicaCatalogEntry rce, boolean overwrite )
  {
    String pfn1 = this.m_pfn;
    String pfn2 = rce.getPFN();
    boolean result = ( pfn1 == null && pfn2 == null ||
		       pfn1 != null && pfn2 != null && pfn1.equals(pfn2) );

    // only merge if PFN match
    if ( result ) {
      String key;
      Object val;

      for ( Iterator i=rce.getAttributeIterator(); i.hasNext(); ) {
	key = (String) i.next();
	val = rce.getAttribute(key);
	if ( hasAttribute(key) ) {
	  if ( overwrite ) setAttribute( key, val );
	} else {
	  setAttribute( key, val );
	}
      }
    }

    return result;
  }

  /**
   * Removes all attributes associated with a PFN.
   * @see #removeAttribute( String )
   */
  public void removeAllAttribute()
  {
    this.m_attributeMap.clear();
  }

  /**
   * Removes a specific attribute.
   *
   * @param name is the name of the attribute to remove.
   * @return the value object that was removed, or <code>null</code>,
   * if the key was not in the map.
   * @see #removeAllAttribute()
   */
  public Object removeAttribute( String name )
  {
    return this.m_attributeMap.remove(name);
  }

  /**
   * Adds a new or overwrites an existing attribute. Note, this is
   * identical to the {@link #addAttribute( String, Object)} method of
   * the same signature.
   *
   * @param key is the name of the attribute
   * @param value is the value object associated with the attribute.
   */
  public void setAttribute( String key, Object value )
  {
    this.m_attributeMap.put( key, value );
  }

  /**
   * Replaces all existing attributes with new attributes. Existing
   * attributes are removed before attempting a shallow copy of the new
   * attributes.
   *
   * @param attributes is the map of new attributes to remember.
   * @see #addAttribute(Map)
   */
  public void setAttribute( Map attributes )
  {
    this.m_attributeMap.clear();
    this.m_attributeMap.putAll(attributes);
  }


  /**
   * Obtains the resource handle from the attributes map. This is a
   * convenience method. Internally, the PFN attribute map is queried
   * for the value of the resource handle.
   *
   * @return the resource handle, or <code>null</code> if unset.
   * @see #setResourceHandle( String )
   */
  public String getResourceHandle()
  {
    return (String) this.m_attributeMap.get( RESOURCE_HANDLE );
  }

  /**
   * Sets a new resource handle to remember as PFN attribute. This is a
   * convenience method. Internally, the PFN attribute map is changed
   * to remember the new resource handle.
   *
   * @param handle is the new resource handle.
   * @see #getResourceHandle()
   */
  public void setResourceHandle( String handle )
  {
    this.m_attributeMap.put( RESOURCE_HANDLE, handle );
  }

  /**
   * Accessor: Obtains the PFN portion from this entry.
   *
   * @return the physical filename, or <code>null</code> if unset.
   * @see #setPFN( String )
   */
  public String getPFN()
  {
    return m_pfn;
  }

  /**
   * Accessor: Sets a new PFN to remember.
   *
   * @param pfn is a new physical filename.
   * @see #getPFN()
   */
  public void setPFN( String pfn )
  {
    m_pfn = pfn;
  }

  /**
   * Converts the contents into a string.
   *
   * @return a textual representation of the item content.
   */
  public String toString()
  {
    // return "(" + m_pfn + "," + m_attributeMap.toString() + ")";
    StringBuffer result = null;

    // save the formatted map content
    String save = m_attributeMap.toString();

    if ( m_pfn == null ) {
      result = new StringBuffer( 10 + save.length() );
      result.append( "((null)," );
    } else {
      result = new StringBuffer( 4 + m_pfn.length() + save.length() );
      result.append( '(' ).append( m_pfn ).append( ',' );
    }

    result.append( save );
    result.append( ')' );

    return result.toString();
  }

  /**
   * Matches two ReplicaCatalogEntry objects. The primary key in this case is
   * the pfn and all the attributes.
   *
   * @return true if the pfn and all the attributes match, false otherwise.
   */
  public boolean equals(Object obj){
      // null check
      if (obj == null)
          return false;

      // see if type of objects match
      if (! (obj instanceof ReplicaCatalogEntry))
          return false;

      ReplicaCatalogEntry rce = (ReplicaCatalogEntry)obj;
      String pfn1 = this.m_pfn;
      String pfn2 = rce.getPFN();

      //rce with null pfns are assumed to match
      boolean result = ( pfn1 == null && pfn2 == null ||
                         pfn1 != null && pfn2 != null && pfn1.equals(pfn2) &&
                         this.getAttributeCount() == rce.getAttributeCount());

      if(result){
          String key;
          Object val;

          //do the matching on attributes now
          for (Iterator it = rce.getAttributeIterator(); it.hasNext(); ) {
              key = (String) it.next();
              val = rce.getAttribute(key);
              if (hasAttribute(key)) {
                  if(!(getAttribute(key).equals(val))){
                      result = false;
                      break;
                  }
              }
          }
      }


      return result;
  }

}
